﻿//
// Author: Ruxo Zheng (http://ruxozheng.spaces.live.com/)
//
// This file is distributed under CPOL 1.0 License (http://www.codeproject.com/info/cpol10.aspx).
//

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using RZ.Collections.Extensions;

namespace RZ.Web
{
    /// <summary>
    /// Content block contains only itself and HtmlContentText.
    /// </summary>
    public class HtmlContentBlock : HtmlContentHeadTag, IEnumerable<HtmlContent>
    {
        C5.ArrayList<HtmlContent> content = new C5.ArrayList<HtmlContent>();
        String fullContent = null;
        String openTag;
        String closeTag;

        public HtmlContentBlock(HtmlContentHeadTag headTag)
            : base(String.Empty, headTag.TagName, headTag.Attributes ?? new HtmlAttributeCollection() , headTag.IsClosed)
        {
            CodeValidator.ThrowArgumentExceptionIf(headTag is HtmlContentBlock);

            this.openTag = headTag.ToString();

            if (this.IsClosed)
                this.closeTag = String.Empty;
        }

        public Int32 Count
        {
            get { return this.content.Count; }
        }

        public HtmlContent this[int index]
        {
            get { return this.content[index]; }
        }

        public Boolean IsEmpty
        {
            get { return this.Count == 0; }
        }

        public void Add(HtmlContentText text)
        {
            ThrowIfAlreadyClosed();
            this.content.Add(text);

            ResetRepresentText();
        }

        public void Add(HtmlContentBlock block)
        {
            ThrowIfAlreadyClosed();
            this.content.Add(block);

            ResetRepresentText();
        }

        public void Close(HtmlContentCloseTag closeTag)
        {
            CodeValidator.ArgumentValidIf(closeTag.TagName == this.TagName, closeTag.TagName + " != " + this.TagName);
            this.IsClosed = true;

            this.closeTag = closeTag.ToString();
            ResetRepresentText();
        }

        /// <summary>
        /// Use when we cannot find the match close tag.
        /// </summary>
        public void ForceClose()
        {
            this.IsClosed = true;

            this.closeTag = "</" + this.TagName + ">";
            ResetRepresentText();
        }

        public Int32 IndexOfTag(String tagName, Int32 startPosition)
        {
            return this.content.FindIndex(
                startPosition,
                item => { var content = item as HtmlContentBlock; return content != null && content.TagName == tagName; }
                );
        }

        #region Find Methods
        struct BlockInfo
        {
            public HtmlContentBlock Block;
            public Int32 Index;

            public HtmlContentBlock InnerBlock
            {
                get { return (HtmlContentBlock) Block[Index]; }
            }

            public BlockInfo(Int32 index, HtmlContentBlock block)
            {
                this.Block = block;
                this.Index = index;
            }
        }

        C5.ArrayList<BlockInfo> BuildFindStack(IList<Int32> startPosition)
        {
            var searchStack = new C5.ArrayList<BlockInfo>(64);

            if (startPosition != null && startPosition.Count > 0)
            {
                var parentBlock = this;

                foreach (Int32 blockIndex in startPosition)
                {
                    var innerBlock = parentBlock[blockIndex] as HtmlContentBlock;

                    if (innerBlock == null)
                        throw new IndexOutOfRangeException("startPosition does not point to HTML content block.");

                    searchStack.Push(new BlockInfo(blockIndex, parentBlock));

                    parentBlock = innerBlock;
                }
            }

            return searchStack;
        }

        Boolean FindTag(String tagName, C5.ArrayList<BlockInfo> searchStack)
        {
            var block = searchStack.IsEmpty? this : searchStack.Last.InnerBlock;
            var contentIndex = 0;

            while (true)
            {
                while (contentIndex < block.Count)
                {
                    var innerBlock = block[contentIndex] as HtmlContentBlock;

                    if (innerBlock == null)
                    {
                        ++contentIndex;
                        continue;
                    }

                    searchStack.Push(new BlockInfo(contentIndex, block));

                    if (innerBlock.TagName == tagName)
                        return true;

                    block = innerBlock;
                    contentIndex = 0;
                }

                if (searchStack.IsEmpty)
                    return false;

                var lastBlock = searchStack.Pop();

                block = lastBlock.Block;
                contentIndex = lastBlock.Index +1;
            }
        }

        /// <summary>
        /// Find tag name from the specified start position.
        /// </summary>
        /// <exception cref="IndexOutOfRangeException">startPosition is invalid.</exception>
        public IList<Int32> FindTag(String tagName, IList<Int32> startPosition)
        {
            var searchStack = BuildFindStack(startPosition);

            if (FindTag(tagName, searchStack))
            {
                var position = from blockInfo in searchStack select blockInfo.Index;

                return position.ToList();
            }
            else
                return null;
        }
        #endregion

        /// <summary>
        /// Currently use on unmatched block.
        /// </summary>
        /// <param name="anotherBlock">Target block to receive content.</param>
        internal void MoveInnerBlockTo(HtmlContentBlock anotherBlock)
        {
            anotherBlock.content.AddAll(this.content);
            this.content.Clear();

            ResetRepresentText();
        }

        #region IEnumerable<HtmlContent> Members

        public IEnumerator<HtmlContent> GetEnumerator()
        {
            return this.content.GetEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        #endregion

        public override string ToString()
        {
            if (this.fullContent == null)
                GenerateFullContent();

            return this.fullContent;
        }

        void ResetRepresentText()
        {
            this.fullContent = null;
        }

        void GenerateFullContent()
        {
            Debug.Assert(this.fullContent == null);

            var allText = new StringBuilder(this.openTag.Length + this.closeTag.Length + 1024);

            allText.Append(this.openTag);

            foreach (HtmlContent innerContent in this.content)
                allText.Append(innerContent.ToString());

            allText.Append(this.closeTag);

            this.fullContent = allText.ToString();
        }

        void ThrowIfAlreadyClosed()
        {
            if (this.IsClosed)
                throw new InvalidOperationException("Block is already closed!");
        }
    }
}
